use std::sync::Arc;

use tokio::sync::Mutex;
use tracing::{debug, error, trace, warn};

use bc_base::{
    BlockHeight, TransactionId, TransactionValue,
    block::Block,
    constants::COINBASE_MATURITY,
    script::interpreter::ScriptInterpreter,
    transaction::{Transaction, TransactionIn, TransactionOut},
};
use bc_lib::top_storage::{
    BlockAndIndexStorageWithCache, MiscItemStorageWithCache, TransactionAndIndexStorageWithCache,
};

use crate::LocalTransaction;
use crate::transaction::{
    TransactionConsensusError, orphan_txs::OrphanTransactions,
    transaction_index::TransactionAndIndexManager, tx_mem_pool::TransactionMemPool,
};

#[derive(Debug)]
pub(crate) struct TransactionConsensus {
    tx_mem_pool: TransactionMemPool,
    orphan_txs: OrphanTransactions,

    tx_and_index_manager: TransactionAndIndexManager,
}

impl TransactionConsensus {
    pub async fn new(
        block_and_index_storage_with_cache: Arc<Mutex<BlockAndIndexStorageWithCache>>,
        tx_and_index_storage_with_cache: Arc<Mutex<TransactionAndIndexStorageWithCache>>,
        misc_item_storage_with_cache: Arc<Mutex<MiscItemStorageWithCache>>,
    ) -> Self {
        let tx_and_index_manager = TransactionAndIndexManager::new(
            block_and_index_storage_with_cache,
            tx_and_index_storage_with_cache,
            misc_item_storage_with_cache,
        )
        .await;

        Self {
            tx_mem_pool: TransactionMemPool::new(),
            orphan_txs: OrphanTransactions::new(),

            tx_and_index_manager,
        }
    }

    // If prev is coinbase, check that it's matured
    pub async fn is_coinbase_mature(
        &self,
        tx_id: &TransactionId,
    ) -> Result<bool, TransactionConsensusError> {
        debug!("Is coinbase({}) mature?", tx_id);
        // Get the block height and best chain height
        let blk_height = self.read_block_height_of_tx(tx_id).await?;
        let best_height = self.tx_and_index_manager.read_best_height().await?;

        trace!(
            "mature height: {}, best height: {}",
            blk_height + COINBASE_MATURITY as BlockHeight,
            best_height
        );

        // Check if the coinbase is mature
        Ok(blk_height + COINBASE_MATURITY as BlockHeight >= best_height)
    }

    pub async fn read_block_height_of_tx(
        &self,
        tx_id: &TransactionId,
    ) -> Result<BlockHeight, TransactionConsensusError> {
        let blk_height = self
            .tx_and_index_manager
            .read_block_height_of_tx(tx_id)
            .await?;
        Ok(blk_height)
    }
}

// for process_transaction
impl TransactionConsensus {
    // bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
    // else if (strCommand == "tx") {
    pub(crate) async fn process_transaction(
        &mut self,
        tx_id: TransactionId,
        tx: Transaction,
        tx_serialize: Vec<u8>,
    ) -> Result<(), TransactionConsensusError> {
        let r = self
            .accept_transaction(tx_id, tx.clone(), tx_serialize, true)
            .await;
        match r {
            Ok(_) => {
                self.process_orphan_tx_recursive(&tx_id).await?;
                Ok(())
            }
            Err(TransactionConsensusError::MissTransactionInput) => {
                self.orphan_txs.add_orphan_tx(&tx_id, tx)?;
                Ok(())
            }
            Err(e) => Err(e),
        }
    }

    // bool CTransaction::AcceptTransaction(CTxDB& txdb, bool fCheckInputs,
    // NOTE: main logic of accept transaction
    async fn accept_transaction(
        &mut self,
        tx_id: TransactionId,
        tx: Transaction,
        tx_serialize: Vec<u8>,
        if_check_inputs: bool,
    ) -> Result<(), TransactionConsensusError> {
        if tx.is_coinbase() {
            return Err(TransactionConsensusError::CoinbaseInLooseTransaction);
        }

        tx.preliminary_check()?;

        // todo: check lock_time // // To help v0.1.5 clients who would see it as a negative number

        if self.tx_mem_pool.contains(&tx_id) {
            return Err(TransactionConsensusError::TransactionAlreadyInPool);
        }

        if if_check_inputs && self.tx_and_index_manager.tx_exists(&tx_id).await {
            return Err(TransactionConsensusError::TransactionAlreadyInDisk(tx_id));
        }

        // Check for conflicts with in-memory transactions
        let old_tx_id = self.check_conflicts_with_in_memory_transactions(&tx)?;

        // Check against previous transactions
        if if_check_inputs {
            self.connect_inputs_of_tx(&tx_id, &tx, 0).await?;
        }

        // Store transaction in memory
        self.add_tx_to_memory_pool(tx_id, tx, tx_serialize)?;

        // are we sure this is ok when loading transactions or restoring block txes
        // If updated, erase old tx from wallet
        if let Some(old_tx_id) = old_tx_id {
            self.remove_tx_from_tx_pool(&old_tx_id)?;
        }

        debug!("Accept Transaction ({})", tx_id);
        Ok(())
    }

    // Recursively process any orphan transactions that depended on this one
    async fn process_orphan_tx_recursive(
        &mut self,
        tx_id: &TransactionId,
    ) -> Result<(), TransactionConsensusError> {
        debug!(
            "Recursively process any orphan transactions that depended on this one({})",
            tx_id
        );
        let mut work_queue = Vec::new();
        work_queue.push(*tx_id);

        let mut index = 0;
        while index < work_queue.len() {
            let prev_tx_id = work_queue[index];
            index += 1;

            let Some(set) = self.orphan_txs.get_orphan_txs_by_prev(&prev_tx_id) else {
                continue;
            };

            let set = set.clone();
            for the_tx_id in &set {
                let tx = self.get_transaction_inner(the_tx_id).await?;
                let tx_serialize = bincode::serialize(&tx).unwrap();
                if let Err(e) = self
                    .accept_transaction(*the_tx_id, tx, tx_serialize, true)
                    .await
                {
                    debug!(
                        "Failed to accept orphan transaction {:?}, error({})",
                        the_tx_id, e
                    );
                } else {
                    // todo: wallet, relay-message

                    work_queue.push(*the_tx_id);
                }
            }
        }

        for it in work_queue {
            self.orphan_txs.remove_orphan_tx(&it)?;
        }

        Ok(())
    }

    // Check for conflicts with in-memory transactions
    fn check_conflicts_with_in_memory_transactions(
        &self,
        tx: &Transaction,
    ) -> Result<Option<TransactionId>, TransactionConsensusError> {
        let mut ret_old_tx: Option<TransactionId> = None;
        for tx_in in tx.get_inputs() {
            let out_point = tx_in.get_prev_out_point();
            let in_point = self.tx_mem_pool.get_tx_in_point(out_point);
            let Some(in_point) = in_point else {
                continue;
            };

            if tx_in != tx.get_inputs().first().unwrap() {
                warn!("Double spending of the same UTXO in transaction pool");
                return Err(TransactionConsensusError::DoubleSpendingUTXOInTxPool);
            }

            // Allow replacing with a newer version of the same transaction
            let old_tx_id = in_point.get_tx_id();
            let old_tx = self.tx_mem_pool.get_tx_from_pool(old_tx_id).unwrap();
            if !tx.is_newer_than(&old_tx.0) {
                warn!("[not newer] Double spending of the same UTXO in transaction pool");
                return Err(TransactionConsensusError::DoubleSpendingUTXOInTxPool);
            }

            self.validate_utxo_spent_by_old_tx(tx, old_tx_id)?;

            ret_old_tx = Some(*old_tx_id);
            break;
        }

        Ok(ret_old_tx)
    }

    fn validate_utxo_spent_by_old_tx(
        &self,
        tx: &Transaction,
        old_tx_id: &TransactionId,
    ) -> Result<(), TransactionConsensusError> {
        for tx_in in tx.get_inputs() {
            let out_point = tx_in.get_prev_out_point();
            let in_point = self.tx_mem_pool.get_tx_in_point(out_point);
            let Some(in_point) = in_point else {
                warn!("[UTXO not in used utxos by txs pool] Not newer of the same transaction");
                return Err(TransactionConsensusError::DoubleSpendingUTXOInTxPool);
            };

            if in_point.get_tx_id() != old_tx_id {
                warn!("[UTXO not used by old-tx] Not newer of the same transaction");
                return Err(TransactionConsensusError::DoubleSpendingUTXOInTxPool);
            }
        }

        Ok(())
    }

    async fn get_transaction_inner(
        &self,
        tx_id: &TransactionId,
    ) -> Result<Transaction, TransactionConsensusError> {
        // check tx exists in memory pool
        if let Some(tx) = self.tx_mem_pool.get_tx_from_pool(tx_id) {
            return Ok(tx.0.clone());
        }

        let tx = self.tx_and_index_manager.read_tx_from_db(tx_id).await?;
        Ok(tx)
    }

    pub async fn get_transaction(
        &self,
        tx_id: &TransactionId,
    ) -> Result<LocalTransaction, TransactionConsensusError> {
        // check tx exists in memory pool
        if let Some(tx) = self.tx_mem_pool.get_tx_from_pool(tx_id) {
            return Ok(LocalTransaction::TransactionInMemPool(tx.0.clone()));
        }

        let tx = self.tx_and_index_manager.read_tx_from_db(tx_id).await?;
        Ok(LocalTransaction::TransactionInDb(tx))
    }

    // Add to memory pool without checking anything.  Don't call this directly,
    // call AcceptTransaction to properly check the transaction first.
    // bool CTransaction::AddToMemoryPool() {
    fn add_tx_to_memory_pool(
        &mut self,
        tx_id: TransactionId,
        tx: Transaction,
        tx_serialize: Vec<u8>,
    ) -> Result<(), TransactionConsensusError> {
        // todo: nTransactionsUpdated++
        self.tx_mem_pool
            .add_tx_to_memory_pool(tx_id, tx, tx_serialize)
    }

    // bool CTransaction::RemoveFromMemoryPool() {
    fn remove_tx_from_tx_pool(
        &mut self,
        tx_id: &TransactionId,
    ) -> Result<(), TransactionConsensusError> {
        // todo: nTransactionsUpdated++
        self.tx_mem_pool.remove_tx_from_tx_pool(tx_id)
    }

    // remove txs that in block from tx pool
    pub async fn remove_transactions_from_tx_pool(&mut self, block: &Block) {
        let merkle_tree = block.get_merkle_tree();
        let transactions = block.get_transactions();
        assert!(transactions.len() == 1 || merkle_tree.len() > transactions.len()); // transactions.len() == 1 --> only contain coinbase tx

        for (index, _tx) in transactions.iter().enumerate() {
            let tx_id = &merkle_tree[index];
            match self.remove_tx_from_tx_pool(tx_id) {
                Err(_) => {
                    continue;
                }
                Ok(_) => {
                    tracing::trace!("Remove Tx({}) from tx pool.", tx_id);
                }
            }
        }
    }
}

// for connect inputs & disconnect inputs
impl TransactionConsensus {
    // bool CTransaction::ConnectInputs(CTxDB& txdb,
    pub async fn connect_inputs_of_tx(
        &mut self,
        tx_id: &TransactionId,
        tx: &Transaction,
        min_fee: TransactionValue,
    ) -> Result<TransactionValue, TransactionConsensusError> {
        assert!(!tx.is_coinbase());

        let mut tx_value_in = 0;
        let tx_out = &tx.get_outputs()[0];
        for tx_in in tx.get_inputs() {
            tx_value_in += self
                .do_check_and_connect_input(tx_id, tx_in, tx_out)
                .await?;
        }

        let tx_fee = tx_value_in - tx.get_output_value();
        if tx_fee < 0 {
            return Err(TransactionConsensusError::NegativeFee);
        }

        if tx_fee < min_fee {
            return Err(TransactionConsensusError::InsufficientFee);
        }

        // todo: add tx to disk index for fblock / add tx to test pool for fminer

        Ok(tx_fee)
    }

    async fn do_check_and_connect_input(
        &mut self,
        tx_id: &TransactionId,
        tx_in: &TransactionIn,
        tx_out: &TransactionOut,
    ) -> Result<TransactionValue, TransactionConsensusError> {
        let prev_out_point = tx_in.get_prev_out_point();
        let prev_tx_id = prev_out_point.get_tx_id();

        let prev_tx = match self.get_transaction_inner(prev_tx_id).await {
            Ok(tx) => tx,
            Err(e) => {
                warn!(
                    "(tx: {}): MissTransactionInput({}). Error message: {}",
                    tx_id, prev_tx_id, e
                );
                return Err(TransactionConsensusError::MissTransactionInput);
            }
        };

        trace!(
            "Check and connect input. tx({}), prev tx({})",
            tx_id, prev_tx_id
        );
        let mut prev_tx_index = self
            .tx_and_index_manager
            .read_tx_index_node(prev_tx_id)
            .await?;

        // trace!("prev_tx_index.get_spent_by().capacity(): {}", prev_tx_index.get_spent_by().capacity());

        let outpoint_index = prev_out_point.get_index();
        let output_len_of_prev_tx = prev_tx.get_outputs().len();
        assert_eq!(output_len_of_prev_tx, prev_tx_index.get_spent_by().len());
        if outpoint_index >= output_len_of_prev_tx as u32
            || outpoint_index >= prev_tx_index.get_spent_by().len() as u32
        {
            error!(
                "(tx: {}): outpoint_index({}) out of range[0, {}). prev tx(id:{}, tx:{:?})",
                tx_id, outpoint_index, output_len_of_prev_tx, prev_tx_id, prev_tx
            );
            return Err(TransactionConsensusError::OutpointIndexOutOfRange);
        }

        // If prev is coinbase, check that it's matured
        if prev_tx.is_coinbase() && !self.is_coinbase_mature(prev_tx_id).await? {
            return Err(TransactionConsensusError::CoinbaseNotMature);
        }

        // Verify signature
        let mut interpreter = ScriptInterpreter::new();
        let mut script = tx_in.get_script_signature().clone();
        script.extend(tx_out.get_script_pubkey().clone());
        let verify_result = interpreter.execute_script(&script, prev_tx_id)?;
        if !verify_result {
            return Err(TransactionConsensusError::InvalidSignature);
        }

        // Check for conflicts
        let outpoint_index = prev_out_point.get_index() as usize;
        if prev_tx_index.is_spent(outpoint_index, tx_id)? {
            error!(
                "Utxo already spent. tx id: {}, prev tx id: {}.",
                tx_id, prev_tx_id
            );
            return Err(TransactionConsensusError::UtxoAlreadySpent);
        }

        // Mark outpoints as spent and Write back
        trace!(
            "Marking outpoint({:?}) as spent by tx({}).",
            prev_out_point, tx_id
        );
        prev_tx_index.set_spent_by(outpoint_index, tx_id)?;
        self.tx_and_index_manager
            .update_tx_index_node_sync(prev_tx_id, prev_tx_index)
            .await?;

        Ok(prev_tx.get_outputs()[prev_out_point.get_index() as usize].get_value())
    }

    // bool CTransaction::DisconnectInputs(CTxDB& txdb)
    pub async fn disconnect_inputs_of_tx(
        &mut self,
        tx_id: &TransactionId,
        tx: &Transaction,
    ) -> Result<(), TransactionConsensusError> {
        assert!(!tx.is_coinbase());

        for tx_in in tx.get_inputs() {
            self.do_disconnect_input(tx_id, tx_in).await?;
        }

        Ok(())
    }

    async fn do_disconnect_input(
        &mut self,
        tx_id: &TransactionId,
        tx_in: &TransactionIn,
    ) -> Result<(), TransactionConsensusError> {
        let prev_out_point = tx_in.get_prev_out_point();
        let prev_tx_id = prev_out_point.get_tx_id();

        let mut prev_tx_index = self
            .tx_and_index_manager
            .read_tx_index_node(prev_tx_id)
            .await?;

        let outpoint_index = prev_out_point.get_index() as usize;
        let len_of_spent_by = prev_tx_index.get_spent_by().len();
        if outpoint_index >= len_of_spent_by {
            error!(
                "do_disconnect_input(tx: {}): outpoint_index({}) out of range[0, {}). prev tx({})",
                tx_id, outpoint_index, len_of_spent_by, prev_tx_id
            );
            return Err(TransactionConsensusError::OutpointIndexOutOfRange);
        }

        // Mark outpoints as not spent
        prev_tx_index.set_spent_by_null(outpoint_index)?;
        self.tx_and_index_manager
            .update_tx_index_node_sync(prev_tx_id, prev_tx_index)
            .await?;
        // self.tx_and_index_manager
        //     .delete_tx_index_node(tx_id)
        //     .await?;

        Ok(())
    }
}

impl TransactionConsensus {
    pub async fn tx_exists_in_db(&self, tx_id: &TransactionId) -> bool {
        self.tx_and_index_manager.tx_exists(tx_id).await
    }

    pub async fn dump_all_txs(&self) {
        self.tx_and_index_manager.dump_all_txs().await;
    }
}

#[cfg(test)]
mod tests_for_learning {
    #[test]
    fn update_vec_in_while_loop() {
        // let mut v = std::collections::VecDeque::from([1, 2, 3, 4, 5]);
        let mut v = vec![1, 2, 3, 4, 5];
        let mut index = 0;
        let mut cnt = 0;
        while index < v.len() {
            if v[index] == 2 {
                // v.push_back(6);
                v.push(6);
            }
            cnt += 1;
            index += 1;
        }

        assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
        assert_eq!(cnt, 6);
    }
}
